<?php

/**
 * Form File Element class
 *
 * @author Mathieu Pheulpin - sokaru91@gmail.com
 *
 */
class Mac_Form_Element_File extends Mac_Form_Element_Abstract
{
    
    /**
    * Constantes relatives aux index de chaque fichier de $_file
    */
    const FILENAME = 'name';
    const FILESIZE = 'size';
    const TMP_NAME = 'tmp_name';
    const FILETYPE = 'type';
    const UL_ERROR = 'error';
    
    const MIN_SIZE = 'minSize';
    const MAX_SIZE = 'maxSize';
    const FORMATS = 'formats';
    const DESTINATION = 'destdir';
    const TAGTIME = 'tagtime';
    const RENAME = 'rename';
    const REQUIRED = 'required';
    const VALIDATORS = 'validators';
    
    /**
     * Constantes de paramètrage
     */
    const TAGTIME_START = 'tagtime_start';
    const TAGTIME_END = 'tagtime_end';
    const TAGTIME_NONE = 'tagtime_none';
    
    /**
     * Rappel des erreur natives d'upload de PHP, correspondant à la clé UL_ERROR
     * 0 < Codes < 100
     *
     * UPLOAD_ERR_OK
     *		Value: 0; There is no error, the file uploaded with success.
     *
     * UPLOAD_ERR_INI_SIZE
     *		Value: 1; The uploaded file exceeds the upload_max_filesize directive in php.ini.
     *
     * UPLOAD_ERR_FORM_SIZE
     *		Value: 2; The uploaded file exceeds the MAX_FILE_SIZE directive that was specified
     *		in the HTML form.
     *
     * UPLOAD_ERR_PARTIAL
     *		Value: 3; The uploaded file was only partially uploaded.
     *
     * UPLOAD_ERR_NO_FILE
     *		Value: 4; No file was uploaded.
     *
     * UPLOAD_ERR_NO_TMP_DIR
     *		Value: 6; Missing a temporary folder. Introduced in PHP 4.3.10 and PHP 5.0.3.
     *
     * UPLOAD_ERR_CANT_WRITE
     *		Value: 7; Failed to write file to disk. Introduced in PHP 5.1.0.
     *
     * UPLOAD_ERR_EXTENSION
     *		Value: 8; A PHP extension stopped the file upload. PHP does not provide a way to
     *		ascertain which extension caused the file upload to stop; examining the list of
     *		loaded extensions with phpinfo() may help. Introduced in PHP 5.2.0.
     */
    
    /**
     * Codes erreurs relatifs aux contrôles natifs
     * 100 < Codes < 200
     */
    const ERROR_MIN_SIZE = 101;
    const ERROR_MAX_SIZE = 102;
    const ERROR_FORMAT = 103;
    const ERROR_DESTINATION = 104;
    const ERROR_UPLOAD = 105;
    const ERROR_MISSING = 106;
    
    /**
     * Code erreur relatif aux validateurs externes
     * 200 < Codes < 300
     *
     * Il est conseillé de paramètrer les validateur de sorte que leurs codes
     * erreur soient dans cette fourchette
     */
    const ERROR_VALIDATE = 200;
    
    /**
     * Tableau associatif contenant les données de $_FILES plus des données
     * de validation et de traitement de chaque ficher du formulaire
     *
     * @var array
     */
    protected $upload = array();
    
    /**
     * Constructeur de la class UploadManager
     *
     * @param string $name input name and id 
     * @param array $options Tableau associatif d'options par défault pour tous les champs input file soumis
     * 						 Les clés sont les constantes de class suivantes :
     * 							self::MIN_SIZE -> Taille minimum en octets (default : pas de minimum)
     * 							self::MAX_SIZE -> Taille maximum en octets (default : pas de maximum)
     * 							self::FORMATS -> array, liste des extensions valides (sans le point) (default: toutes les extensions valides)
     * 							self::DESTINATION -> Repertoire de destination des fichiers uploadés (default: aucun)
     * 							self::TAGTIME -> TAGTIME_START|TAGTIME_START|TAGTIME_NONE Ajoute time() dans le nom du fichier au début ou à la fin (default: TAGTIME_NONE)
     * 							self::REQUIRED -> bool, indique si les fichiers sont obligatoires (default: false)
     * 							self::VALIDATORS -> object|array, un ou plusieurs validateurs à appliquer à tous les champs (class: Mac_Form_Validator_File_Abstract, default: aucun)
     */
    public function __construct($name, array $options = array())
    {
        parent::__construct($name);
        
        $this->upload = isset($_FILES[$name]) ? $_FILES[$name] : array();
        
        $this->upload[self::MIN_SIZE] = (isset($options[self::MIN_SIZE])) ? $options[self::MIN_SIZE] : null;
        $this->upload[self::MAX_SIZE] = (isset($options[self::MAX_SIZE])) ? $options[self::MAX_SIZE] : null;
        $this->upload[self::FORMATS] = (isset($options[self::FORMATS])&&is_array($options[self::FORMATS])) ? $options[self::FORMATS] : null;
        $this->upload[self::DESTINATION] = (isset($options[self::DESTINATION])) ?
                                               ((substr($options[self::DESTINATION], -1)=='/') ?
                                                   substr($options[self::DESTINATION], 0, -1) :
                                                    $options[self::DESTINATION]) :
                                            null;
        $this->upload[self::TAGTIME] = (isset($options[self::TAGTIME])) ? $options[self::TAGTIME] : self::TAGTIME_NONE;
        $this->upload[self::RENAME] = (isset($options[self::RENAME])) ? $options[self::RENAME] : null;
        $this->upload[self::REQUIRED] = (isset($options[self::REQUIRED])) ? (bool)$options[self::REQUIRED] : false;

        // Il faut gérer le validateur en dernier pour que les autres paramètres lui soient transmis
        $this->upload[self::VALIDATORS] = array();
        if (isset($options[self::VALIDATORS])) {
            if (is_array($options[self::VALIDATORS])) {
                $validators = $options[self::VALIDATORS];
            } else {
                $validators = array($options[self::VALIDATORS]);
            }
            foreach ($validators as $validator) {
                $thisValidator = clone $validator;
                $thisValidator->setFile($files[$name]);
                $this->upload[self::VALIDATORS][] = $thisValidator;
            }
        }
    }
    
    /**
     * Indique la taille requise (min et max) pour certains ou tous les fichiers uploadés
     *
     * @param null|int $min Taille minimum en octets (null: pas de minimum)
     * @param null|int $max Taille maximum en octets (null: pas de maximum)
     * @return Mac_Form_Element_File
     */
    public function setValidSize($min, $max)
    {
        $this->upload[self::MIN_SIZE] = ($min>0) ? $min : null;
        $this->upload[self::MAX_SIZE] = ($max>0) ? $min : null;
        return $this;
    }
    
    /**
     * Indique les extensions accéptées pour un ou plusieurs fichiers uploadés
     *
     * @param array $exts Liste des extensions valides (sans le point, ex: 'txt')
     * @return Mac_Form_Element_File
     */
    public function setValidExtensions(array $exts)
    {
        $this->upload[self::FORMATS] = $exts;
        return $this;
    }
    
    /**
     * Ajoute une extension valide à la liste pour un ou plusieurs fichiers uploadés
     *
     * @param string $ext Extension valide (sans le point, ex: 'txt')
     * @return Mac_Form_Element_File
     */
    public function addValidExtension($ext)
    {
        if (!in_array($ext, $this->upload[self::FORMATS])) {
            $this->upload[self::FORMATS][] = $ext;
        }
        return $this;
    }
    
    /**
     * Paramètre le repertoire de destination d'un ou plusieurs fichiers uploadés
     *
     * @param string $path Chemin vers le dossier
     * @return Mac_Form_Element_File
     */
    public function setDestinationDirectory($path)
    {
        $this->upload[self::DESTINATION] = $path;
        return $this;
    }
    
    /**
     * Paramètre la position du tagtime dans le nom du fichier uploadé
     *
     * @param TAGTIME_START|TAGTIME_END|TAGTIME_NONE $pos Position (début, fin, ancun)
     * @return Mac_Form_Element_File
     */
    public function setTagtimePosition($pos)
    {
        $this->upload[self::TAGTIME] = $pos;
        return $this;
    }
    
    /**
     * Indique si un ou plusieurs fichiers sont requis dans le formulaire, si un fichier requis
     * n'est pas fourni, les autres fichiers ne seront pas téléchargés sur le serveur
     *
     * @param bool $required Obligatoire (true) ou non (false)
     * @return Mac_Form_Element_File
     */
    public function setRequired ($required)
    {
        $this->upload[self::REQUIRED] = (bool)$required;
        return $this;
    }
    
    /**
     * Configure le renommage d'un fichier avant upload vers le serveur
     *
     * @param string $newname Nouveau nom du fichier (sans l'extension qui sera conservée), peut être combiné au tagtime
     * @return Mac_Form_Element_File
     */
    public function renameFile ($newname)
    {
        $this->upload[self::RENAME] = $newname;
        return $this;
    }
    
    /**
     * Ajoute un validateur à la liste des validateurs appliqués à un ficher uploadé
     *
     * @param Mac_Form_Validator_File_Abstract $validator Validateur à appliquer
     * @return Mac_Form_Element_File
     */
    public function addValidator (Mac_Form_Validator_File_Abstract $validator)
    {
        $thisValidator = clone $validator;
        $thisValidator->setFile($this->upload);
        $this->upload[self::VALIDATORS][] = clone $thisValidator;
        return $this;
    }
    
    /**
     * Paramètre une série d'options pour un champ input file du formulaire
     *
     * @param array $options Tableau d'options, même fonctionnement que dans le constructeur avec le renommage en plus (self::RENAME)
     */
    public function setOptions (array $options)
    {
        if (isset($options[self::MIN_SIZE])) {
            $this->upload[self::MIN_SIZE] = $options[self::MIN_SIZE];
        }
        if (isset($options[self::MAX_SIZE])) {
            $this->upload[self::MAX_SIZE] = $options[self::MAX_SIZE];
        }
        if (isset($options[self::FORMATS])&&is_array($options[self::FORMATS])) {
            $this->upload[self::FORMATS] = $options[self::FORMATS];
        }
        if (isset($options[self::DESTINATION])) {
            if (substr($options[self::DESTINATION], -1)=='/') {
                $this->upload[self::DESTINATION] = substr($options[self::DESTINATION], 0, -1);
            } else {
                $this->upload[self::DESTINATION] = $options[self::DESTINATION];
            }
        }
        if (isset($options[self::TAGTIME])) {
            $this->upload[self::TAGTIME] = $options[self::TAGTIME];
        }
        if (isset($options[self::RENAME])) {
            $this->upload[self::RENAME] = $options[self::RENAME];
        }
        if (isset($options[self::REQUIRED])) {
            $this->upload[self::REQUIRED] = (bool)$options[self::REQUIRED];
        }

        // Il faut gérer le validateur en dernier pour que les autres paramètres lui soient transmis
        if (isset($options[self::VALIDATORS])) {
            $this->upload[self::VALIDATORS] = array();
            if (is_array($options[self::VALIDATORS])) {
                $validators = $options[self::VALIDATORS];
            } else {
                $validators = array($options[self::VALIDATORS]);
            }
            foreach ($validators as $validator) {
                $thisValidator = clone $validator;
                $thisValidator->setFile($this->upload);
                $this->upload[self::VALIDATORS][] = $thisValidator;
            }
        }
        return $this;
    }
    
    /**
     * Copie les fichiers uploadés dans leur(s) répertoire(s) de destination
     *
     * @return array
     */
    public function download ()
    {
        if ($this->isValid()) {
            $result = false;
            
            $uploaded_filename = basename($this->upload[self::FILENAME]);

            $xplod_filename = explode('.', $uploaded_filename);
            $extension = end($xplod_filename);

            if ($this->upload[self::RENAME]!==null) {
                $clearname = $this->upload[self::RENAME];
            } else {
                array_pop($xplod_filename); // On enlève l'extension du nom du fichier
                $clearname = implode('.', $xplod_filename);
            }

            if ($this->upload[self::TAGTIME] == self::TAGTIME_END) {
                $newname = $clearname.'_'.date('YmdHis').'.'.$extension;
            } elseif ($this->upload[self::TAGTIME] == self::TAGTIME_START) {
                $newname = date('YmdHis').'_'.$clearname.'.'.$extension;
            } else {
                $newname = $clearname.'.'.$extension;
            }
             
            $uploadfile = $this->upload[self::DESTINATION].'/'.$newname;
            if (move_uploaded_file($this->upload[self::TMP_NAME], $uploadfile)) {
                $result = $newname;
            } elseif ($this->upload[self::TMP_NAME]!='') {
                $this->messages[] = self::ERROR_UPLOAD;
            }
            
            return $result;
        }
    }
    
    /**
     * Vérifie que toutes les contraintes parametrées sur les fichiers uploadés sont respectées
     * et remplie le tableau d'erreurs lorsque ce n'est pas le cas
     *
     * @return bool
     */
    public function isValid()
    {
        $file_uploaded = isset($this->upload[self::FILENAME]);
        if (!$file_uploaded && ($this->upload[self::REQUIRED] === false)) {
            return true;
        } else if (!$file_uploaded && ($this->upload[self::REQUIRED] === true)) {
            $this->messages[] = self::ERROR_MISSING;
            return false;
        } else {
            $validatorsOk = true;
            foreach ($this->upload[self::VALIDATORS] as $validator) {
                $thisValidatorOk = $validator->isValid();
                if (!$thisValidatorOk) {
                    $this->messages[] = $validator->getErrorCode();
                }
                $validatorsOk &= $thisValidatorOk;
            }
            if (!$validatorsOk) {
                $this->messages[] = self::ERROR_VALIDATE;
            }
    
            $uploadOk = ( isset($this->upload[self::UL_ERROR]) && (($this->upload[self::UL_ERROR]==UPLOAD_ERR_OK) || ($this->upload[self::UL_ERROR]==UPLOAD_ERR_NO_FILE)));
            if (!$uploadOk) {
                $this->messages[] = isset($this->upload[self::UL_ERROR]) ? $this->upload[self::UL_ERROR] : self::ERROR_MISSING;
            }
    
            $minSizeOk = (
                ( (($this->upload[self::MIN_SIZE] !== null)&&($this->upload[self::FILESIZE] >= $this->upload[self::MIN_SIZE])) || ($this->upload[self::MIN_SIZE] === null))
                || (($this->upload[self::FILESIZE] == 0) && ($this->upload[self::REQUIRED] === false)) );
            if (!$minSizeOk) {
                $this->messages[] = self::ERROR_MIN_SIZE;
            }
    
            $maxSizeOk = ((($this->upload[self::MAX_SIZE] !== null) && ($this->upload[self::FILESIZE] <= $this->upload[self::MAX_SIZE])) || ($this->upload[self::MAX_SIZE] === null));
            if (!$maxSizeOk) {
                $this->messages[] = self::ERROR_MAX_SIZE;
            }
    
            $explodedFilename = explode(".", $this->upload[self::FILENAME]);
            $extOk = (
                ( ($this->upload[self::FORMATS]!==null) && (in_array(strtolower(end($explodedFilename)), $this->upload[self::FORMATS])) )
                || ($this->upload[self::FORMATS]===null)
                || (($this->upload[self::FILENAME]==='') && ($this->upload[self::REQUIRED]===false))
                || ($this->upload[self::UL_ERROR]!=UPLOAD_ERR_OK) );
            if (!$extOk) {
                $this->messages[] = self::ERROR_FORMAT;
            }
    
            $destOk = ( ($this->upload[self::DESTINATION]!==null) && (is_dir($this->upload[self::DESTINATION])) );
            if (!$destOk) {
                $this->messages[] = self::ERROR_DESTINATION;
            }
    
            $requiredOk = ( ($this->upload[self::REQUIRED]===false) || ($this->upload[self::UL_ERROR]==UPLOAD_ERR_OK) );
            if (!$requiredOk && $uploadOk) {
                $this->messages[] = self::ERROR_MISSING;
            }
    
            return ($validatorsOk && $uploadOk && $minSizeOk && $maxSizeOk && $extOk && $destOk && $requiredOk);
        }
    }
    
    /**
     * Is a file uploaded in this field
     */
    protected function isEmpty()
    {
        return !(isset($this->upload[self::FILENAME]));
    }
    
    public function __toString()
    {
        return '<input
            type="file"
            id="'.$this->id.'"
            name="'.$this->name.'"
            '.((count($this->class) > 0) ? 'class="'.implode(' ', $this->class).'"' : '').'
            '.$this->printOptions().'
        />';
    }
}